﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace CoveSmushbox
{
    public enum SmsSplitTypes
    {
        /// <summary>
        /// Do not split the message.
        /// Messages longer than 160 characters will be truncated.
        /// </summary>
        None,

        /// <summary>
        /// Split the message into multiple messages.
        /// Divide the message on word boundaries.
        /// </summary>
        ByWord,

        /// <summary>
        /// Split the message into multiple messages.
        /// Divide the message on sentence boundaries.
        /// </summary>
        BySentence,

        /// <summary>
        /// Split the message into multiple messages.
        /// Divide the message on new line boundaries.
        /// </summary>
        ByLine
    }

    static class StringExtensions
    {
        
        /// <summary>
        /// Splits a single message into multiple messages if it's > 160 characters using various split algorithms.
        /// </summary>
        public static IEnumerable<string> SmsSplit(this string message, SmsSplitTypes splitType = SmsSplitTypes.ByWord)
        {
            if (String.IsNullOrWhiteSpace(message))
            {
                // No valid message
                return new string[] { };
            }

            message = message.Trim();

            if (message.Length < 160)
            {
                // Message is not too long, no need to split
                return new string[] { message };
            }

            if (splitType == SmsSplitTypes.None)
            {
                return new string[] { message.Substring(0, 160) };
            }

            // Tokenize the string
            var tokens = Tokenize(message, splitType);

            // Join the tokens
            return JoinTokens(tokens, splitType);
        }

        #region RegexBuddy
        // [^.!?\s][^.!?]*(?:[.!?](?!['"]?\s|$)[^.!?]*)*[.!?]?['"]?(?=\s|$)
        // 
        // Match a single character NOT present in the list below «[^.!?\s]»
        //    One of the characters “.!?” «.!?»
        //    A whitespace character (spaces, tabs, and line breaks) «\s»
        // Match a single character NOT present in the list “.!?” «[^.!?]*»
        //    Between zero and unlimited times, as many times as possible, giving back as needed (greedy) «*»
        // Match the regular expression below «(?:[.!?](?!['"]?\s|$)[^.!?]*)*»
        //    Between zero and unlimited times, as many times as possible, giving back as needed (greedy) «*»
        //    Match a single character present in the list “.!?” «[.!?]»
        //    Assert that it is impossible to match the regex below starting at this position (negative lookahead) «(?!['"]?\s|$)»
        //       Match either the regular expression below (attempting the next alternative only if this one fails) «['"]?\s»
        //          Match a single character present in the list “'"” «['"]?»
        //             Between zero and one times, as many times as possible, giving back as needed (greedy) «?»
        //          Match a single character that is a “whitespace character” (spaces, tabs, and line breaks) «\s»
        //       Or match regular expression number 2 below (the entire group fails if this one fails to match) «$»
        //          Assert position at the end of the string (or before the line break at the end of the string, if any) «$»
        //    Match a single character NOT present in the list “.!?” «[^.!?]*»
        //       Between zero and unlimited times, as many times as possible, giving back as needed (greedy) «*»
        // Match a single character present in the list “.!?” «[.!?]?»
        //    Between zero and one times, as many times as possible, giving back as needed (greedy) «?»
        // Match a single character present in the list “'"” «['"]?»
        //    Between zero and one times, as many times as possible, giving back as needed (greedy) «?»
        // Assert that the regex below can be matched, starting at this position (positive lookahead) «(?=\s|$)»
        //    Match either the regular expression below (attempting the next alternative only if this one fails) «\s»
        //       Match a single character that is a “whitespace character” (spaces, tabs, and line breaks) «\s»
        //    Or match regular expression number 2 below (the entire group fails if this one fails to match) «$»
        //       Assert position at the end of the string (or before the line break at the end of the string, if any) «$»
        #endregion
        
        private static readonly Regex _sentenceRegEx = new Regex(@"[^.!?\s][^.!?]*(?:[.!?](?!['""]?\s|$)[^.!?]*)*[.!?]?['""]?(?=\s|$)", RegexOptions.Compiled);
        private static readonly Regex _lineRegEx = new Regex(@"^.+$", RegexOptions.Compiled | RegexOptions.Multiline);

        private static IEnumerable<string> Tokenize(string message, SmsSplitTypes splitType)
        {
            switch (splitType)
            {
                case SmsSplitTypes.BySentence:
                    var matches = _sentenceRegEx.Matches(message)
                                                .Cast<Match>()
                                                .Where((m) => !String.IsNullOrWhiteSpace(m.Value))
                                                .Select((m) => m.Value.Trim() + " ");
                    if (matches.Any())
                    {
                        return matches;
                    }
                    else
                    {
                        // No sentences found
                        return new string[] { message };
                    }

                case SmsSplitTypes.ByLine:
                    matches = _lineRegEx.Matches(message)
                                        .Cast<Match>()
                                        .Where((m) => !String.IsNullOrWhiteSpace(m.Value))
                                        .Select((m) => m.Value.TrimEnd('\r', '\n') + "\r\n");
                    if (matches.Any())
                    {
                        return matches;
                    }
                    else
                    {
                        // No sentences found
                        return new string[] { message };
                    }

                case SmsSplitTypes.ByWord:
                    return message.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Select((s) => s + " ");

                default:
                    throw new ArgumentException("splitType");
            } 
        }

        private static IEnumerable<string> JoinTokens(IEnumerable<string> tokens, SmsSplitTypes splitType, int totalResults = 0)
        {
            var results = new List<string>();

            if (tokens == null || !tokens.Any())
            {
                return results;
            }

            var index = 1;
            var currentMessage = new StringBuilder();
            currentMessage.Append(string.Format("({0}/{1}) ", index, totalResults));

            var firstToken = true;
            var lastToken = false;
            foreach (var token in tokens)
            {
                // New line optimization
                var currentToken = token.Replace("\r\n", "\n");

                if (!firstToken && currentMessage.Length + currentToken.Length > 160)
                {
                    if (currentMessage.Length + currentToken.TrimEnd('\n', ' ').Length <= 160)
                    {
                        // Trimming the last token will fit it into the current message, so do it
                        lastToken = true;
                    }
                    else
                    {
                        // Appending the next token would make the message too long, and this is not the first token
                        // Return the constructed message
                        // Each message should always contain at least one token
                        results.Add(currentMessage.ToString().TrimEnd('\n', ' '));

                        // Reset current message state
                        currentMessage = new StringBuilder();
                        index += 1;
                        currentMessage.Append(string.Format("({0}/{1}) ", index, totalResults));

                        firstToken = true;
                    }
                }

                // Append the next token
                if (firstToken && currentMessage.Length + currentToken.Length > 160)
                {
                    // This is the first token, returning a blank message does not make sense so we have to trim it and append it
                    // Calculate how much length is left
                    var lengthLeft = 160 - currentMessage.Length;

                    // For "..."
                    lengthLeft -= 3;

                    var toAppend = currentToken.Substring(0, lengthLeft) + "...";

                    currentMessage.Append(toAppend);
                }
                else
                {
                    // Append the next token
                    if (lastToken)
                    {
                        // This is the last token, trim its end and append it
                        currentMessage.Append(currentToken.TrimEnd('\n', ' '));
                        lastToken = false;
                    }
                    else
                    {
                        currentMessage.Append(currentToken);
                    }
                }

                firstToken = false;
            }

            // All tokens processed, so add the last message
            results.Add(currentMessage.ToString().TrimEnd('\n', ' '));

            if (totalResults < results.Count)
            {
                // We've processed these tokens with the incorrect total results
                // Do it again, but this time we know the total results
                //
                // Note that increasing the totalResults might actually lead to having more results than we've calculated this time,
                // since the number of digits in the "total" (count/total) might increase. In that case, we'll simply do this twice.
                return JoinTokens(tokens, splitType, results.Count);
            }
            else
            {
                return results;
            }
        }

        private static Regex _notAllowedCharactersRegEx = new Regex(@"[^a-z0-9 _!@$!""#%&'()*+,\-./:;<=>?\r\n]", RegexOptions.IgnoreCase | RegexOptions.Compiled);

        /// <summary>
        /// Sanitizes some text to be sent via SMS by replacing invalid characters.
        /// </summary>
        public static string SanitizeForSms(this string message)
        {
            message = _notAllowedCharactersRegEx.Replace(message, "_");

            return message;
        }
    }
}
